Skip to content

fix(runtime,plugin-security): the declared-webhook activation door switches one of two holders a namespace cannot tell apart, and the upgrade report names each withheld holder - #33204

Merged
objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/project-thread-esw9si
Nov 14, 2026
Merged

objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/project-thread-esw9si

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Nov 14, 2026 •

Copy link
Copy Markdown
Contributor

epic:#31527#3 · os-tesla · session_01L9qibzECS2npZFku4956Um

Requested by Tesla · project thread

Refs #33016 (items 1, 2, 3, 4, 5, 6, 8)

Clause-②: yes (widening)
Metadata-protocol: none

维护者速读

  • 改了什么:声明式 webhook 的激活门 POST /security/_activation/webhook/NAME 新增 ?package=PKG 精确限定,能单独开关同名的两个持有者之一;升级影响报告对每个持有者(含被禁用的第二个)各报一条,并给出该持有者的开关地址 ?package=PKG(被禁用的持有者须先启用其包再开关);两条平台测试清单与 liveness 证据改到新的 declared:PKG/NAME 引用。
  • 为什么改:同一命名空间的两个共有包(或无命名空间的包旁边另有持有者)同时声明一个 webhook 名时两者都在派发,但裸地址与命名空间地址都只答 409,运维无法单独关掉其中一个;两条清单项会把正确的产品判失败。
  • 风险与回滚:只放宽一个原本 400 的查询参数,其余答复不变;回滚即 revert 本 PR,不涉及数据迁移。
  • 先看:packages/runtime/src/domains/catalog-activation.ts(resolveExactWebhook 与查询集)、packages/plugins/plugin-security/src/upgrade-impact-report.ts(webhookShippers)、packages/runtime/src/domains/catalog-activation-door.test.ts。
  • 两分钟验证:pnpm --filter @objectstack/runtime exec vitest run src/domains/catalog-activation-door.test.ts,应见两个注册顺序下 ?package= 各写自己的一行、两扇门都 409 点名双方。
  • 需要你做的:无,席位落地。

What changes

Item 1 (S1), the door. Two holders the namespace address cannot tell apart, two co-owners of one namespace or a package that declares no namespace beside another holder, are both served and both dispatched, and both webhook/NAME and webhook/NS/NAME answer 409 naming both, so neither could be switched. The bare address now takes ?package=PKG (resolveExactWebhook, WEBHOOK_DOOR_QUERY_PARAMS in packages/runtime/src/domains/catalog-activation.ts), the exact qualifier /meta reads (ADR-0150 D2) and the permission-set door already takes: it addresses exactly PKG's served declaration and writes PKG's own (webhook, package_id, name) row (WEBHOOK_LEDGER_ADDRESSING), the row naming no package left as it is under two holders and taken over under one (D8, webhookAdoptsPackageless). A PKG that does not serve the name, a disabled one included, is 409 RESOURCE_CONFLICT not-held-by-qualifier naming the served holders, or 404 when nothing serves it. package is the one query parameter of the bare webhook address (AGENTS route rule 5, read by the shared readPermissionSetPackageQuery); the namespace address takes none.

FROM → TO rows (Clause-② widening):

  • POST /security/_activation/webhook/NAME?package=PKG, PKG serving NAME: FROM 400 VALIDATION_ERROR (unknown query parameter) TO 200, PKG's row written, data.packageId = PKG.
  • Same, PKG not serving NAME: FROM 400 TO 409 RESOURCE_CONFLICT naming the holders (or 404 when no enabled package declares NAME). Nothing written.
  • POST /security/_activation/webhook/NS/NAME?package=…: 400 before and after.

Item 2 (S3). Pinned: B disabled beside A enabled, B's namespace address is 409 naming A (no code change).

Item 3 (S5). integration-system.webhook-lifecycle (steps[1]) and cli.organization-ownership-ceremony (acceptance[5].verify) expect ref_id declared:com.example.showcase/showcase_task_changed, as packages/qa/dogfood/test/upgrade-18x-harness.dogfood.test.ts pins; each item's revision and history bumped.

Item 4 (nit 1). packages/spec/liveness/webhook.json name evidence: one slot per served (package, name) holder and declaredWebhookRefId(name, packageId).

Item 5 (nit 2). The upgrade-impact report's switch line (SWITCH_DOORS) and withheld-webhook line name webhook/NAME?package=PACKAGE, where they named the namespace address. It addresses the finding's package whatever its namespace, once that package is enabled: a disabled package holds nothing for the door, so its address answers 409 until it is enabled again (item 6). Every webhook finding names a package (a re-enablement's row records one, a withheld webhook's holder is a package), so the line carries no bare-address arm.

Item 6 (nit 3). webhookShippers also counts each holder packageScopedHoldersOf(…, { includeDisabledPackages: true }) answers, confirmed against its own code artifact (shipsAt), so a disabled second holder gets its withheld finding. FROM one finding (the holder the bare artifact lookup answered) TO one finding per holder. A disabled holder's finding is marked packageDisabled: true (an added optional field), and the withheld-webhook line says such a package must be enabled first, then switched at ?package=PACKAGE, so the line never sends the operator to a door that answers 409.

Item 7. Not in this PR: a security item is with the owner seat.

Item 8 (nit 4). Pinned: one holder with its own row beside a row naming no package, the bare address writes the holder's own row (no code change).

Responsibility. Producing code: the runtime activation door (handleCatalogActivationWrite) and plugin-security's boot report (collectUpgradeImpactFindings). Existing platform path: none switched one of two holders a namespace cannot tell apart; a package disable or a hand-written ledger row were the only stops. Reached by: an operator (manage_metadata, the platform operator under a wall) whose deployment holds two such webhook holders, rare; the report and checklist lines by every boot and every checklist run.

Rulings quoted. Epic ruling 6842178655 on PR #32577 ("Follow-up card (p2), not this PR") files these items; ADR-0150 D2 (?package= is the exact qualifier), D4 (one row per holder), D5 (qualified addresses), D8 (a row naming no package is the one holder's). Surface grep ruling|Ruling-ref|maintainer|⛔ hits: catalog-activation.ts 8 (ADR-0131 seeder ruling, #22621 → A, ADR-0150 D2 ruling, epic ruling MF1 ×3, four ⛔ door limits), upgrade-impact-report.ts 1 (the ruling on #24443), webhook.json 3 (the #7131 previews ruling, the spec-only override note); none is touched by this diff.

File-surface breaches (seat extended the surface): packages/runtime/src/route-ledger.ts (one sentence on the webhook/:ns/:name row naming ?package=) and content/docs/automation/webhooks.mdx (one sentence naming the exact qualifier).

Red lines

  1. Resolution is never loosened. The bare and namespace addresses of two holders still refuse 409 naming both, in both registration orders (catalog-activation-door.test.ts › [FROM 409 TO switched] … registered alpha, beta and … registered beta, alpha); ?package= resolves only an exact served holder and refuses every other PKG naming the holders (… a package that does not hold the name is 409 …, … a disabled package holds nothing …); the bare read of two co-owners still throws the 409 naming both (declared-webhooks.test.ts › two co-owners of one namespace, registered alpha, beta / beta, alpha).
  2. Ambiguity is reported, never guessed. Under two holders a ?package= write never takes over the row naming no package ([null, false] kept beside the holder's own row in both orders); the report names each holder rather than the first one found (upgrade-impact-report.test.ts › [FROM one TO each] registered com.acme.crm, com.acme.addon … and the reversed order).
  3. Independently releasable. The phase-1 one-holder pins stay green (CONTROL — one holder: the bare address answers as before …, CONTROL — one holder: its namespace address writes the same row, CONTROL — one holder: \?package=` writes the row the bare address writes …, [FROM 409 TO written] one holder with its own row beside a row naming no package …`); bare-name URL behaviour is unchanged (every pre-existing door test unmodified and green); no migration and no ADR-0087 entry: no stored shape or authorable key moves.

Tests

  • New: catalog-activation-door.test.ts 10 cases (S3, the ?package= describe ×8: two co-owners of one namespace and a package with no namespace beside a holder, each in both orders, nit 4); declared-webhooks.test.ts 4 (two co-owners, and a package with no namespace beside a namespaced holder, each in both orders: both dispatched under their own ids, the bare read refused naming both); upgrade-impact-report.test.ts 2 (disabled second holder, both orders, its finding marked packageDisabled and the line saying to enable first) + 1 rewritten (the remedy names the exact-qualifier address and no bare-address arm).
  • Ablations (each mutation landed on disk via scripts/ablation-replace.mjs, test imports resolve to src/, restored blob == HEAD):
    • the webhook query set put back to empty ⇒ 7 red (registered alpha, beta, registered beta, alpha, a package with no namespace beside a holder ×2, a package that does not hold the name …, a disabled package holds nothing …, CONTROL — one holder: ?package= …);
    • the disabled-holder count removed from webhookShippers ⇒ 2 red (both registration orders);
    • the namespace address put back in the switch line ⇒ 1 red (… the remedy names the exact-qualifier address);
    • the packageDisabled mark removed, or the enable-first sentence removed from the line ⇒ 2 red each (both registration orders).

Acceptance notes

  • plugin-webhooks' own remedies (the name guard and the redelivery veto, declaredWebhookActivationPath) still name the slot's namespace address, as ruled for objectql: narrow the phase-1 one-holder refusal from environment to namespace for webhook, connector and email_template (ADR-0150 child B, webhook family) #31699 round 2; for two co-owners of one namespace that address answers 409, and ?package= is the one that switches. Not changed here (a ruled text); the seat files the follow-up.
  • Measured, no code change: the environment's own declaration (no package) beside a packaged holder of the same name. In the registry (the booted shape) a package-less entry beside one packaged holder is that holder's customization (ADR-0150 D3), not a second holder: one slot, switchable as before. Beside two packaged holders it binds to no package, is in neither slot and is not dispatched. When the declarations come from the metadata service (no registry item), the name is refused whole: neither declaration is dispatched, the bare address answers 409 naming the environment and the package, ?package=PKG and the namespace address answer 404, nothing is written. No holder has a working switch there, and nothing is delivered either. Left to the seat.
  • Item 7's probe result is with the owner seat.

Verification

All readings at head 0afa1e462 (the review's patch round on c68c098f0; branch base 4c152685, origin/main f45c0125 merged once; full pnpm build green at c68c098f0, 119/119 tasks).

Tests and typecheck
  • pnpm --filter @objectstack/runtime typecheck && test: Test Files 597 passed (597), Tests 10619 passed, 19 skipped; lock VERDICT command-exit 0.
  • pnpm --filter @objectstack/plugin-security test: Test Files 267 passed, Tests 6607 passed (45 skipped); pnpm --filter @objectstack/plugin-webhooks test: Test Files 25 passed, Tests 296 passed.
  • typecheck of @objectstack/runtime, @objectstack/plugin-security, @objectstack/plugin-webhooks: exit 0.
Gates
  • node scripts/pm/dispatch-gates.mjs --commands derived 105 families (the same 105 as at c68c098f0); all 105 run with stdin closed, every exit code 0; --ran reconciliation: 105 derived, 105 run, 0 NOT-MEASURED, 0 UNRUN.
  • Outside the derivation, also run: pnpm lint exit 0; pnpm --filter @objectstack/spec check:generated exit 0.
  • At c68c098f0 the first run (before the merge) found two real problems, fixed in 95e17a456: check:platform-checklist wanted each item's top-level revision bumped with its history entry. Its three exit-3 prerequisite refusals (check:skill-examples, check:dual-build-cjs-loads, the teardown-shape self-test's fixture commit) and check:dts-closure were local build-state, cleared by the full build and the fixture fetch; all four exit 0 on the final run.
Ablations

Each via node scripts/ablation-replace.mjs (anchor 1 → 0 on disk, restored blob == HEAD, git diff HEAD empty); the suites import src/ directly.

  1. catalogActivationQueryParams: the webhook bare address's package taken away ⇒ catalog-activation-door.test.ts 7 failed / 84 passed.
  2. webhookShippers: the disabled-holder loop's add removed ⇒ upgrade-impact-report.test.ts 2 failed / 43 passed (at c68c098f0; the loop is unchanged since).
  3. WEBHOOK_SWITCH_DOOR: the namespace address put back ⇒ 1 failed / 44 passed.
  4. The withheld finding's packageDisabled spread deleted ⇒ upgrade-impact-report.test.ts 2 failed / 43 passed.
  5. The enable-first sentence of the withheld-webhook line replaced ⇒ 2 failed / 43 passed.

dogfood_battery: not run (no ! in the title and no (narrowing) arm).


Generated by Claude Code

🤖 Generated with Claude Code

https://claude.ai/code/session_01L9qibxGWY9FF7Lgr4CiTa3

…mespace address cannot tell apart, and the upgrade report names each disabled holder

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L9qibxGWY9FF7Lgr4CiTa3
…er switch and the disabled co-holder; docs name the door

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L9qibxGWY9FF7Lgr4CiTa3
@github-actions

github-actions Bot commented Nov 14, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 3 package(s): @objectstack/plugin-security, @objectstack/runtime, @objectstack/spec, touching 18 documentable anchor(s). ⚠️ 1 changed file(s) yielded no anchor (packages/spec/liveness/webhook.json), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

5 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/automation/webhooks.mdx _(via /api/v1/security/_activation/webhook (route, a path literal in CATEGORY_LINES; a path literal in SWITCH_DOORS; a path literal in WEBHOOK_SWITCH_DOOR), /security/_activation/:type/:name (route, a path literal in a comment on a changed line), /security/activation/webhook/:name (route, a path literal in a comment on a changed line))
  • content/docs/concepts/metadata-lifecycle.mdx _(via /security/activation/:type/:name (route, a path literal in a comment on a changed line))
  • content/docs/kernel/runtime-services/email-service.mdx (via callerPackageId (symbol, a field of const object base))
  • content/docs/permissions/authorization.mdx _(via /security/activation/:type/:name (route, a path literal in a comment on a changed line))
  • content/docs/permissions/sharing-rules.mdx _(via handleCatalogActivationWrite (symbol, a top-level function), /security/activation/:type/:name (route, a path literal in a comment on a changed line))
What this run could not see
  • 1 changed file(s) yielded no anchor (packages/spec/liveness/webhook.json) — pages documenting those are invisible to this run
  • 5 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 65 of 247 client-bound route-ledger rows — the other 182 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 182: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 59 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 123 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 179 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 67274acad689d5be75d5bfc5007f4fb9396aa907 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from cd5284edd0ae3a7da95c8ee8bb98e40128b1ce15 — the merge of head 0afa1e4627236ecff76807c9c4e81996d6c7c603 into base 67274acad689d5be75d5bfc5007f4fb9396aa907, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin cd5284edd0ae3a7da95c8ee8bb98e40128b1ce15 && git checkout cd5284edd0ae3a7da95c8ee8bb98e40128b1ce15
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 67274acad689d5be75d5bfc5007f4fb9396aa907 0afa1e4627236ecff76807c9c4e81996d6c7c603 && git checkout -B drift-repro 67274acad689d5be75d5bfc5007f4fb9396aa907 && git merge --no-ff 0afa1e4627236ecff76807c9c4e81996d6c7c603

node scripts/docs-audit/affected-docs.mjs --json 67274acad689d5be75d5bfc5007f4fb9396aa907

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 67274acad689d5be75d5bfc5007f4fb9396aa907 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface labels Nov 14, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

epic:#31527#3 · os-tesla · session_01L9qibzECS2npZFku4956Um
Posted: 2026-11-14T02:23Z

Contract review

Served-tier: TIER_DEFAULT
Head-sha: c68c098f0fee5c8d969b63f9dbce78ed9a94ca66
Local-runs: none

Inputs: card #33016 (body, triage audit 6847357630, claim 6868795624, dev report 6870083520), PR #33204 (body, 11 files, net diff against merge-base f45c0125 of main), and the head's 45 check-runs. The PR head is still c68c098f0. The file list decides no review face; the changeset's Clause-②: yes (widening) line makes this the declaration face.

① Derived judgments

Accept-set and public-surface changes the diff implies, each judged:

  1. POST /security/_activation/webhook/NAME?package=PKG, PKG serving NAME: FROM 400 VALIDATION_ERROR TO 200, PKG's own (webhook, package_id, name) row written, data.packageId in the answer. Right. resolveExactWebhook matches the served slots by exact package id only; nothing is inferred. Pinned in both registration orders (catalog-activation-door.test.ts), with the row naming no package left untouched under two holders.
  2. Same address, PKG not serving NAME (a disabled package included): FROM 400 TO 409 RESOURCE_CONFLICT not-held-by-qualifier naming the served holders, or 404 when nothing serves the name; nothing written. Right, and it is the refusal shape the namespace address already gives. Pinned.
  3. One holder, ?package= its own id: writes the row the bare address writes and adopts the row naming no package through the same webhookAdoptsPackageless count (every holder, disabled included) as the namespace arm. Right (D8 unchanged). Pinned as a CONTROL.
  4. Closed query set: package alone on the webhook's bare address, none on webhook/NS/NAME, none on every other non-permission type; empty or repeated package is 400. Right (route rule 5). Pinned, the namespace address and sharing_rule included.
  5. Bare webhook/NAME and webhook/NS/NAME with no query: unchanged, still 409 naming both under two holders, in both orders. Right — red line 1 holds; red line 3 holds, since the only door answer that moves is a former 400.
  6. Upgrade-impact report, withheldWebhooks: FROM one finding (the holder the bare artifact lookup answers) TO one finding per holder, a disabled second holder included, each confirmed by shipsAt. Right, card item 6. The production registry carries packageScopedHoldersOf (the method is optional only for test doubles). The finding set's digest moves for a deployment in that state, so it is notified once more; that follows from the declared row.
  7. Upgrade-impact report log lines (SWITCH_DOORS, withheld-webhook line): the namespace address replaced by webhook/NAME?package=PACKAGE, or the bare address for a finding naming no package. Right as far as card item 5 asks, with finding S2 below.
  8. Exported surface of @objectstack/runtime: WEBHOOK_DOOR_QUERY_PARAMS added; catalogActivationQueryParams and readPermissionSetPackageQuery each gain one optional trailing parameter; @objectstack/plugin-security's UpgradeImpactRegistry gains one optional method. Right: all additive, no existing caller changes meaning (permission-set-clone.ts reads the same message as before).
  9. Checklist items integration-system.webhook-lifecycle steps[1] and cli.organization-ownership-ceremony acceptance[5].verify now expect declared:com.example.showcase/showcase_task_changed. Right: it is what upgrade-18x-harness.dogfood.test.ts pins for a fresh delivery; revisions and history bumped; the one remaining bare reference in cli.json is a dated history entry and correctly stays.
  10. packages/spec/liveness/webhook.json name evidence, the route-ledger.ts note and content/docs/automation/webhooks.mdx: prose only, each agrees with the code read. Right.
  11. Card items 2 and 8: pins only, no answer moves. Right (B disabled beside A enabled is 409 naming A; one holder with its own row beside a row naming no package writes its own row).

Findings (none is a must-fix):

  • S1 · should-fix. Card item 1 names three states; the pins build one (two co-owners of one namespace). The state the PR title and changeset also claim, a package that declares no namespace beside another holder, has no pin for ?package= or for both holders being dispatched. The code read is namespace-agnostic, so no defect is read; the claim is simply unpinned.
  • S2 · should-fix. Items 5 and 6 meet badly: the report now emits a finding for a DISABLED holder and its line sends the operator to ?package=PACKAGE, which this PR's own pin answers 409 for a disabled package. The PR body's "every packaged finding can use" is therefore overstated. Not a regression (on main the namespace address refused the disabled holder too), so text or a follow-up, not a blocker.
  • N1 · nit. catalog-activation.ts module doc still reads "package for permission, nothing for every other type", which the paragraph added a few lines below contradicts.
  • N2 · nit. WithheldDeclaredWebhook.packageId is always a string, so "a finding that names no package" cannot occur on the withheld-webhook line; the clause is true only of the shared SWITCH_DOORS line.
  • N3 · nit. The changeset's first FROM/TO row does not mention data.packageId in the 200 (the PR body does).

② Semver level

  • Clause-②: yes (widening) and Metadata-protocol: none stand line-initial in both the PR body and the changeset, and they agree. Correct.
  • Metadata-protocol: none is the right derivation: the diff adds nothing a metadata author can write (no key, value or type); a query parameter on an HTTP door is not authorable metadata. The claim's expected added was a forecast, and the diff does not bear it out.
  • (widening) is right: the only moved door answers are former 400s; nothing that answered 2xx, 404 or 409 on main answers differently. No BREAKING banner or ADR-0087 marker is owed.
  • Level: none takes patch, and a non-protocol widening ships patch. @objectstack/runtime: patch and @objectstack/plugin-security: patch match what the diff publishes from each.
  • Nit N4: packages/spec/liveness/ is in @objectstack/spec's published files, and the evidence text changes, but @objectstack/spec is not named in the changeset. The fixed version group bumps it with the others, so only its changelog line is missing. Check Changeset concluded success.

③ Boundary flags

  • Dev open_questions: empty. Nothing to answer.
  • Deviation, file surface: packages/runtime/src/route-ledger.ts and content/docs/automation/webhooks.mdx, one sentence each, declared in the PR body as extended by the seat. Both are accurate and needed by the new address. Accepted; the seat's own extension message is not among this review's inputs, so the seat confirms it.
  • Deviation, Metadata-protocol derived none against the claim's added: answered in ②, agreed.
  • Deviation, card item 7 not in the PR: the PR says a security item is with the owner seat. Not reviewed here. The PR says Refs, not Closes, so the card stays open for it. Accepted.
  • Out-of-scope finding carried by the dev (plugin-webhooks' own remedies, declaredWebhookActivationPath, still name the namespace address, which answers 409 for two co-owners): escalated to the seat. Now that a door exists that switches those holders, a remedy that names one that refuses is the same defect card item 5 fixed in the report; it is a ruled text, so it needs a follow-up card, not an edit here.
  • Escalated to the seat, with S2: decide whether a disabled holder's finding should name a door at all.
  • Escalated to the seat, as a question: the environment's own declaration beside a packaged holder of the same name has no package id for ?package= to name. The card does not list that state and this review did not establish how the bare address answers it; if it is 409, that holder still has no switch.
  • Claim line "Contract review: this seat, at CONTRACT_REVIEW_TIER": the committed tree touches no published schema and no governed path (Governed Surface Queue Guard success), so the review owed is the declaration face at TIER_DEFAULT, as rendered here.

Check-runs on c68c098f0 (45): 17 success, 9 skipped, 0 failed, 19 still running.

  • Success: Auto Label, Dogfood Regression Gate, Plan Test Core shards, Plan Test UI shards, filter, Lint · PM dispatch-gates, Lint · ESLint, Lint · scoped · tooling self-tests, Lint · scoped · query-options erasure, Type Check · source gates, Spec property liveness, Governed Surface Queue Guard, Check PR Size, Check Documentation Links, Check Changeset, PR-body guards, Flag docs affected by code changes.
  • Skipped: the Dogfood Regression Gate matrix row, Temporal Conformance, UI Published Artifacts, Dogfood Verify CLI, Schedule gate, Packed-tarball smoke, Borrowed queue verdict, Queue verdict, 18.x upgrade harness.
  • Still running, not judged: Test Core (1/8) to (8/8), Build Core, Build Docs, Build UI Deps, Live E2E UI, Type Check · workspace, Type Check · debt ledger, Type Check · consumer gates, Lint · CI, release and engine gates, Lint · scoped · slot lookup and corpus, Lint · repo and tooling gates, Lint · docs and code guards.

The verdict below is on the contract. The seat waits for every running check to conclude green before it lands anything; a red one voids nothing here but blocks the landing. The PR is still a draft.

Implemented-by: claude/project-thread-esw9si
Reviewed-by: session_01L9qibzECS2npZFku4956Um

VERDICT: PASS

…ys to enable it first; pin the holder with no namespace

The withheld-webhook finding of a disabled package is marked packageDisabled
and its line says the package must be enabled before its exact-qualifier
address switches it. The report's webhook switch drops the bare-address arm no
finding reaches. Pins a package declaring no namespace beside another holder,
in both registration orders, at the door and in the dispatcher. The door's
module doc names the webhook's query parameter; the changeset names the
response's packageId and the spec liveness text.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L9qibxGWY9FF7Lgr4CiTa3
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

epic:#31527#3 · os-tesla · session_01L9qibzECS2npZFku4956Um
Posted: 2026-11-14T03:18Z

Contract review

Served-tier: TIER_DEFAULT
Head-sha: 0afa1e4627236ecff76807c9c4e81996d6c7c603
Local-runs: none

Delta round: this record replaces 6870153173 (on c68c098f0) and is a full record on the new head. Inputs: card #33016 (body and its four comments, the dev's patch-round report 6870833323 included), PR #33204 (the replaced body, 11 files, net diff against merge-base f45c0125 of main; the one new commit 0afa1e46 read on its own as well), and the new head's check-runs. The file list still decides no review face; the changeset's Clause-②: yes (widening) line makes this the declaration face.

① Derived judgments

Accept-set and public-surface changes the net diff implies, each judged:

  1. POST /security/_activation/webhook/NAME?package=PKG, PKG serving NAME: FROM 400 VALIDATION_ERROR TO 200, PKG's own (webhook, package_id, name) row written, data.packageId in the answer. Right. resolveExactWebhook matches served slots by exact package id; nothing is inferred. Unchanged since the last head.
  2. Same address, PKG not serving NAME (a disabled package included): FROM 400 TO 409 RESOURCE_CONFLICT not-held-by-qualifier naming the served holders, or 404 when nothing serves the name; nothing written. Right.
  3. One holder, ?package= its own id: the row the bare address writes, the row naming no package adopted by the same every-holder count as the namespace arm. Right (D8 unchanged).
  4. Closed query set: package alone on the webhook's bare address, none on webhook/NS/NAME or any other non-permission type. Right (route rule 5); the module doc now says the same (N1 folded).
  5. Bare webhook/NAME and webhook/NS/NAME with no query: unchanged, 409 naming both under two holders. Right — red line 1 holds; red line 3 holds, the only door answers that move are former 400s.
  6. Upgrade-impact report, withheldWebhooks: FROM one finding TO one per holder, a disabled second holder included. Right, card item 6.
  7. New in this round: WithheldDeclaredWebhook.packageDisabled?: true, set only when isPackageDisabled answers true for the finding's package. Right, and additive: optional, absent on every finding that existed before (an enabled holder's finding is byte-for-byte what it was), and it has no other reader in the repository. It enters the finding-set digest only for findings that are themselves new in this PR.
  8. Report log lines: SWITCH_DOORS and the withheld-webhook line name webhook/NAME?package=PACKAGE alone (the bare-address arm is gone, N2 folded); the withheld line adds that a finding marked packageDisabled does not fire now and its package must be enabled first, then switched there. Right, with nit N5.
  9. Exported surface: WEBHOOK_DOOR_QUERY_PARAMS added, one optional trailing parameter on catalogActivationQueryParams and on readPermissionSetPackageQuery, one optional method on UpgradeImpactRegistry, one optional field on WithheldDeclaredWebhook. Right: all additive.
  10. Checklist items, liveness evidence, route-ledger.ts note, webhooks.mdx: unchanged since the last head, right as judged there.

Findings of record 6870153173, each re-judged on this head:

  • S1 · answered. The second state is pinned in both registration orders. Door (catalog-activation-door.test.ts): a package outside every namespace's owner list beside a namespaced holder, the bare address 409 naming both with nothing written, then ?package= writes each holder's own row and leaves the row naming no package as it was. Dispatcher (declared-webhooks.test.ts): a package installed with no namespace beside a namespaced one, both dispatched under their own declared:PKG/NAME ids, list() showing one with a namespace and one without, the bare read refused naming both, the exact read answering the no-namespace holder.
  • S2 · answered. The disabled holder's finding is marked and the line no longer sends the operator straight to a door that refuses; pinned in both orders, with the enabled holder pinned as unmarked. The PR body's items 5 and 6 are corrected.
  • N1, N2, N3, N4 · folded. Module doc, the dropped bare-address arm (its test now asserts the arm is absent), data.packageId in the changeset's first row, and @objectstack/spec: patch with a liveness-text line.

New findings (none is a must-fix):

  • N5 · nit. The withheld line and the packageDisabled jsdoc say the disabled package's address "answers 409". This PR's own pin shows 404 when the disabled package is the name's only holder. "Refuses" would be true of both.
  • N6 · nit. The PR body's 维护者速读 first bullet still says the report gives a switch address every finding can use; items 5 and 6 below it now say otherwise.

② Semver level

  • Clause-②: yes (widening) and Metadata-protocol: none stand line-initial in both the PR body and the changeset, and agree. Correct.
  • Metadata-protocol: none still holds: this round adds a field to a boot report's finding and prose; nothing a metadata author can write changes.
  • (widening) still holds: no answer that was 2xx, 404 or 409 on main moves; the new field is optional. No BREAKING banner or ADR-0087 marker is owed.
  • Level: none takes patch. @objectstack/runtime, @objectstack/plugin-security and now @objectstack/spec are each patch, and each publishes something from this diff (the door, the report, the liveness evidence text under spec's published liveness/). Matches.
  • Check Changeset is still running on this head and is not judged here.

③ Boundary flags

  • Dev open_questions (both reports): empty.
  • File-surface extension to packages/runtime/src/route-ledger.ts and content/docs/automation/webhooks.mdx: seat-approved (the seat's card-thread message of 01:18Z, as the seat states for this round). Closed.
  • Metadata-protocol derived none against the claim's added: agreed, as in ②.
  • Card item 7 not in the PR: the PR says a security item is with the owner seat. Not reviewed here; the PR says Refs, so the card stays open for it.
  • The environment's own declaration beside a packaged holder (raised in 6870153173): measured by the dev in 6870833323, no code change. In the registry it is the one holder's customization, or bound to no package beside two; on the metadata-service path the name is refused whole, nothing is dispatched, no address switches it, and ?package=PKG answers a 404 whose sentence misreads. The seat files it as a follow-up; not in this PR. Accepted as out of this card's scope.
  • plugin-webhooks' own remedies still naming the namespace address (a ruled text): the seat files the follow-up, per the PR's acceptance notes. Accepted.
  • Tier: no published schema and no governed path in the tree (Governed Surface Queue Guard success on this head), so the declaration face at TIER_DEFAULT stands.

Check-runs on 0afa1e462 (41 rows, two runs of the PR-automation workflow among them): 0 failed.

  • Success: PR-body guards (both runs), Auto Label, Check PR Size, Dogfood Regression Gate, filter, Lint · PM dispatch-gates, Lint · scoped · tooling self-tests, Flag docs affected by code changes, Governed Surface Queue Guard, Spec property liveness, Check Documentation Links.
  • Skipped: the second run's Auto Label and Check PR Size, the Dogfood Regression Gate matrix row, Temporal Conformance, UI Published Artifacts, Dogfood Verify CLI, Schedule gate, Packed-tarball smoke, Borrowed queue verdict, Queue verdict, 18.x upgrade harness.
  • Still running, not judged: Check Changeset (both runs), Plan Test Core shards (the Test Core shards are not created yet), Plan Test UI shards, Build Core, Build Docs, Build UI Deps, Live E2E UI, Type Check · workspace, Type Check · source gates, Type Check · debt ledger, Type Check · consumer gates, Lint · ESLint, Lint · CI, release and engine gates, Lint · scoped · slot lookup and corpus, Lint · scoped · query-options erasure, Lint · repo and tooling gates, Lint · docs and code guards.

The verdict below is on the contract. The seat waits for every running check, the Test Core shards included, to conclude green before it lands anything. The PR is still a draft.

Implemented-by: claude/project-thread-esw9si
Reviewed-by: session_01L9qibzECS2npZFku4956Um

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

epic:#31527#3 · os-tesla · session_01L9qibzECS2npZFku4956Um

席位核对 · ACCEPT · PR #33204 (#33016 items 1 to 6, 8) · 2026-11-14T03:43Z

Head-sha: 0afa1e4627236ecff76807c9c4e81996d6c7c603 · report 6870083520, round-1 delta 6870833323 · claim 6868795624

  • Form: draft → main, first body line Refs #33016 (items 1, 2, 3, 4, 5, 6, 8). Item 7 is with the owner seat. Files: one changeset, runtime catalog-activation.ts + door test, plugin-security upgrade-impact report + test, plugin-webhooks test, two checklist areas, packages/spec/liveness/webhook.json, plus packages/runtime/src/route-ledger.ts (one note sentence) and content/docs/automation/webhooks.mdx (one sentence), a surface extension this seat approved in the card thread at 01:18Z.
  • Red lines (epic design(metadata): address behavioural metadata by package — key flows, actions, jobs and other non-object types by (package, name); re-open ADR-0028 (replaces #30806 phase 3 renames) #31527): ① bare webhook/NAME and webhook/NS/NAME answer as before (409 naming both under two holders); ?package= matches served holders by exact package id, nothing inferred. ② no answer reads registration order; every two-holder pin runs both orders. ③ the only door answers that move are former 400s; the one-holder answer, its row and the package-less adoption are unchanged; the report's new field is optional and absent on every pre-existing finding.
  • Contract review: declaration face (Clause-②: yes (widening)), TIER_DEFAULT, isolated reviewer: record 6870153173 at c68c098f0 (PASS, S1 and S2 should-fix, N1 to N4), delta record 6870855978 at this head (PASS, S1 and S2 answered, N1 to N4 folded, N5 and N6 nits).
  • Accepted nits: N6 fixed by this seat in the PR body (维护者速读, first bullet). N5 (the withheld line says a disabled package's address "answers 409"; with that package the name's only holder it is 404) is accepted and goes to the follow-up card on this door's wording.
  • Changeset: @objectstack/runtime, @objectstack/plugin-security, @objectstack/spec patch; Metadata-protocol: none (a door query parameter, nothing a metadata author writes) agrees in body and changeset.
  • Out-of-scope findings, held by the seat: (1) plugin-webhooks' name guard and redelivery veto still name the namespace address; (2) on the metadata-service path an environment declaration beside one packaged holder of the same name has no working switch (measured in 6870833323). Both go to a follow-up card.
  • Gates: check-runs at this head: 62 success, 14 skipped, 0 other (read this act). Local (report 6870833323): runtime 10619, plugin-security 6607, plugin-webhooks 296 passed; typecheck and lint 0; dispatch-gates 105/105 --ran 0 NOT-MEASURED; ablations red and restored.
  • Landing: K = 9 (git rev-list --count --first-parent f45c0125..origin/main), no churn in the touched files since base; no locale pack. Landed on the maintainer's standing word 「按 pm-dispatch 审过的pr你可以自主落地」 (2026-10-30 14:20Z): pr_ready + automerge_enable in this stroke.

Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review November 14, 2026 03:44
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Nov 14, 2026
Merged via the queue into main with commit 1f8ce6e Nov 14, 2026
77 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-esw9si branch November 14, 2026 04:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants